'    Copyright 2008 Jan Molnar <jan.molnar@abds.de>
'
'    This file is part of JCQ.

'    JCQ is free software: you can redistribute it and/or modify
'    it under the terms of the GNU General Public License as published by
'    the Free Software Foundation, either version 2 of the License, or
'    (at your option) any later version.

'    JCQ is distributed in the hope that it will be useful,
'    but WITHOUT ANY WARRANTY; without even the implied warranty of
'    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
'    GNU General Public License for more details.

'    You should have received a copy of the GNU General Public License
'    along with JCQ. If not, see <http://www.gnu.org/licenses/>.

Imports JCsTools.JCQ.IcqInterface.DataTypes

''' <summary>
''' Provides the state for an authentication cookie request.
''' </summary>
''' <remarks></remarks>
Public Class RequestAuthenticationCookieState

    Private _BosServerAddress As String

    ''' <summary>
    ''' Gets or sets the Address of the Sign In Server.
    ''' </summary>
    Public Property BosServerAddress() As String
        Get
            Return _BosServerAddress
        End Get
        Set(ByVal value As String)
            _BosServerAddress = value
        End Set
    End Property

    Private _AuthCookie As List(Of Byte)

    ''' <summary>
    ''' Gets or sets the Authentication Cookie.
    ''' </summary>
    Public Property AuthCookie() As List(Of Byte)
        Get
            Return _AuthCookie
        End Get
        Set(ByVal value As List(Of Byte))
            _AuthCookie = value
        End Set
    End Property

    Private _AuthenticationSucceeded As Boolean

    ''' <summary>
    ''' Gets or sets a value indicating whether the sign in succeeded.
    ''' </summary>
    Public Property AuthenticationSucceeded() As Boolean
        Get
            Return _AuthenticationSucceeded
        End Get
        Set(ByVal value As Boolean)
            _AuthenticationSucceeded = value
        End Set
    End Property

    Private _AuthenticationError As IcqInterface.DataTypes.AuthFailedCode

    ''' <summary>
    ''' Gets or sets the authentication error.
    ''' </summary>
    Public Property AuthenticationError() As IcqInterface.DataTypes.AuthFailedCode
        Get
            Return _AuthenticationError
        End Get
        Set(ByVal value As IcqInterface.DataTypes.AuthFailedCode)
            _AuthenticationError = value
        End Set
    End Property
End Class

''' <summary>
''' This task requests an authentication cookie for a username and a password.
''' </summary>
Public Class RequestAuthenticationCookieTask
    Inherits Core.BasicAsyncTask
    Implements Core.Interfaces.ITaskWithState(Of RequestAuthenticationCookieState)

    Private ReadOnly _PasswordCredential As Interfaces.IPasswordCredential
    Private ReadOnly _State As RequestAuthenticationCookieState
    Private ReadOnly _Connector As IcqConnector

    Public Sub New(ByVal owner As IcqConnector, ByVal credential As Interfaces.IPasswordCredential)
        _Connector = owner
        _State = New RequestAuthenticationCookieState
        _PasswordCredential = credential

        AddHandler Connector.FlapReceived, AddressOf OnFlapReceived
    End Sub

    ''' <summary>
    ''' Gets the connector used to process this task.
    ''' </summary>
    Public ReadOnly Property Connector() As IcqConnector
        Get
            Return _Connector
        End Get
    End Property

    ''' <summary>
    ''' Gets the password credential used to request the authentication cookie.
    ''' </summary>
    Public ReadOnly Property PasswordCredential() As Interfaces.IPasswordCredential
        Get
            Return _PasswordCredential
        End Get
    End Property

    ''' <summary>
    ''' Gets the current state of the task.
    ''' </summary>
    Public ReadOnly Property State() As RequestAuthenticationCookieState Implements Core.Interfaces.ITaskWithState(Of RequestAuthenticationCookieState).State
        Get
            Return _State
        End Get
    End Property

    Protected Overrides Sub SetCompleted()
        MyBase.SetCompleted()

        ' When the task is completed we don't have to listen for new
        ' flaps anymore.
        RemoveHandler Connector.FlapReceived, AddressOf OnFlapReceived
    End Sub

    Protected Overrides Sub PerformOperation()
        ' The only required operation is sending the cookie request
        SendAuthenticationCookieRequest(PasswordCredential.Password)
    End Sub

    ''' <summary>
    ''' Filters FalpReceived events and passes the appropiate data to analyzation methods.
    ''' </summary>
    Private Sub OnFlapReceived(ByVal sender As Object, ByVal e As FlapTransportEventArgs)
        Dim flap As Flap = e.Flap

        Try
            ' we can ignore flaps other than connection closed negotiations
            If flap.Channel <> FlapChannel.CloseConnectionNegotiation Then Return

            AnalyzeConnectionClosedFlap(flap)
        Catch ex As Exception
            JCsTools.Core.Kernel.Exceptions.PublishException(ex)
        End Try
    End Sub

    ''' <summary>
    ''' Analyzes connection closed negotiation flaps.
    ''' </summary>
    Private Sub AnalyzeConnectionClosedFlap(ByVal flap As Flap)
        Dim tlvsByTypeNumer As Dictionary(Of Integer, Tlv)

        ' we are only interested in tlvs and their type number.
        tlvsByTypeNumer = (From x In flap.DataItems _
                               Where TypeOf x Is Tlv _
                               Select DirectCast(x, Tlv)).ToDictionary(Function(tlv) tlv.TypeNumber)

        If tlvsByTypeNumer.ContainsKey(&H5) And tlvsByTypeNumer.ContainsKey(&H6) Then
            ' if these tlvs are present the authentication succeeded and everything is okay :)

            Dim bosServerTlv = DirectCast(tlvsByTypeNumer(&H5), TlvBosServerAddress)
            Dim authCookieTlv = DirectCast(tlvsByTypeNumer(&H6), TlvAuthorizationCookie)

            State.BosServerAddress = bosServerTlv.BosServerAddress
            State.AuthCookie = authCookieTlv.AuthorizationCookie
            State.AuthenticationSucceeded = True

            SetCompleted()
        ElseIf tlvsByTypeNumer.ContainsKey(&H8) Then
            ' if this tlv is present the authentication has failed.

            Dim authFailedTlv = DirectCast(tlvsByTypeNumer(&H8), TlvAuthFailed)

            Core.Kernel.Logger.Log("IcqConnector", TraceEventType.Error, _
                                   "Connection to server failed. ErrorSubCode: {0}", authFailedTlv.ErrorSubCode.ToString)

            State.AuthenticationSucceeded = False
            State.AuthenticationError = authFailedTlv.ErrorSubCode

            SetCompleted()
        Else
            ' in all other cases something went wrong ...
            State.AuthenticationSucceeded = False

            SetCompleted()
        End If
    End Sub

    ''' <summary>
    ''' Sends an authentication cookie request to the server.
    ''' </summary>
    Private Sub SendAuthenticationCookieRequest(ByVal password As String)
        Dim flapRequestCookie As FlapRequestSignInCookie

        flapRequestCookie = New FlapRequestSignInCookie

        ' TODO: Supply correct client information.
        flapRequestCookie.ScreenName.Uin = Connector.Context.Identity.Identifier
        flapRequestCookie.Password.Password = password
        flapRequestCookie.ClientIdString.ClientIdString = "SomeClientSoftware"
        flapRequestCookie.ClientId.ClientId = 8123
        flapRequestCookie.ClientMajorVersion.ClientMajorVersion = 3
        flapRequestCookie.ClientMinorVersion.ClientMinorVersion = 9
        flapRequestCookie.ClientLesserVersion.ClientLesserVersion = 7
        flapRequestCookie.ClientBuildNumber.ClientBuildNumber = 8
        flapRequestCookie.ClientDistributionNumber.ClientDistributionNumber = 1
        flapRequestCookie.ClientLanguage.ClientLanguage = "en"
        flapRequestCookie.ClientCountry.ClientCountry = "us"

        Connector.Send(flapRequestCookie)
    End Sub
End Class

Partial Public Class IcqConnector
    Inherits BaseConnector
    Implements Interfaces.IConnector

    Private isSigningIn As Boolean

    Public Event SignInCompleted(ByVal sender As Object, ByVal e As System.EventArgs) Implements Interfaces.IConnector.SignInCompleted

    Public Event SignInFailed(ByVal sender As Object, ByVal e As Interfaces.SignInFailedEventArgs) Implements Interfaces.IConnector.SignInFailed

    Public Event Disconnected(ByVal sender As Object, ByVal e As Interfaces.DisconnectedEventArgs) Implements Interfaces.IConnector.Disconnected

    Public Sub New(ByVal context As Interfaces.IContext)
        MyBase.New(context)

        RegisterSnacHandler(Of Snac0101)(&H1, &H1, AddressOf AnalyseSnac0101)
        RegisterSnacHandler(Of Snac0103)(&H1, &H3, AddressOf AnalyseSnac0103)
        RegisterSnacHandler(Of Snac0107)(&H1, &H7, AddressOf AnalyseSnac0107)
        RegisterSnacHandler(Of Snac0118)(&H1, &H18, AddressOf AnalyseSnac0118)

        RegisterSnacHandler(Of Snac1306)(&H13, &H6, AddressOf AnalyseSnac1306)
    End Sub

    Public Sub SignIn(ByVal credential As Interfaces.ICredential) Implements Interfaces.IConnector.SignIn
        Dim password As Interfaces.IPasswordCredential

        Dim serverEndpoint As System.Net.IPEndPoint

        Dim requestAuthCookieTask As RequestAuthenticationCookieTask

        password = TryCast(credential, Interfaces.IPasswordCredential)
        If password Is Nothing Then Throw New ArgumentException("Credential musst be of Type IPasswordCredential", "credential")

        Try
            isSigningIn = True

            ' Connect to the icq server and get a bos server address and and authentication cookie.
            InnerConnect()

            requestAuthCookieTask = New RequestAuthenticationCookieTask(Me, password)

            requestAuthCookieTask.Run()

            ' When the task is run, we can exspect a disconnect ...
            TcpContext.SetCloseExpected()

            requestAuthCookieTask.WaitCompleted()

            If Not requestAuthCookieTask.State.AuthenticationSucceeded Then
                ' The authentication attempt was not successfull. There are many reasons for this
                ' to occur for example wrong password, to many connections etc.
                ' The client needs to be informed that the sign in failed.

                OnSignInFailed(requestAuthCookieTask.State.AuthenticationError.ToString)
                isSigningIn = False
                Return
            End If

            ' if the authentication attempt was successfull we can connect to the bos server
            ' and send the just received authentication cookie to begin the sign in procedure.

            serverEndpoint = ConvertServerAddressToEndPoint(requestAuthCookieTask.State.BosServerAddress)

            InnerConnect(serverEndpoint)

            SendAuthenticationCookie(requestAuthCookieTask.State.AuthCookie)
        Catch
            isSigningIn = False

            Throw
        End Try
    End Sub

    Public Sub SignOut() Implements Interfaces.IConnector.SignOut
        ' TODO: Implement proper Sign out.
        Throw New NotImplementedException
    End Sub

    ''' <summary>
    ''' Sends the cookie received from the authentication server to the bos server.
    ''' The server replies with SnacXXX and initiates the sign in procedure.
    ''' </summary>
    ''' <remarks></remarks>
    Private Sub SendAuthenticationCookie(ByVal authenticationCookie As List(Of Byte))
        Dim flapSendCookie As FlapSendSignInCookie

        flapSendCookie = New FlapSendSignInCookie
        flapSendCookie.AuthorizationCookie.AuthorizationCookie.AddRange(authenticationCookie)

        Send(flapSendCookie)
    End Sub

    Private Sub BaseInternalDisconnected(ByVal sender As Object, ByVal e As DisconnectEventArgs) Handles Me.InternalDisconnected
        'TODO: Provide disconnect messages
        OnDisconnected("Server closed connection.", e.IsExpected)
    End Sub

    Private Sub OnDisconnected(ByVal message As String, ByVal expected As Boolean)
        RaiseEvent Disconnected(Me, New Interfaces.DisconnectedEventArgs(message, expected))
    End Sub

    Private Sub OnSignInFailed(ByVal message As String)
        RaiseEvent SignInFailed(Me, New Interfaces.SignInFailedEventArgs(message))
    End Sub

    Private Sub OnSignInCompleted()
        RaiseEvent SignInCompleted(Me, EventArgs.Empty)
    End Sub

    Private Sub AnalyseSnac0101(ByVal dataIn As Snac0101)
        Try
            If isSigningIn Then OnSignInFailed(String.Format("Error: {0}, Sub Code: {1}", _
                                                dataIn.ErrorCode.ToString, dataIn.SubError.ErrorSubCode))

            Throw New IcqException(dataIn.ServiceId, dataIn.ErrorCode, dataIn.SubError.ErrorSubCode)
        Catch ex As Exception
            Core.Kernel.Exceptions.PublishException(ex)
        End Try
    End Sub

    Private Sub AnalyseSnac0103(ByVal dataIn As Snac0103)
        Dim requiredVersions As List(Of Integer)
        Dim notSupported As List(Of Integer)

        Try
            requiredVersions = New List(Of Integer)(New Integer() {&H1, &H2, &H3, &H4, &H9, &H13, &H15})
            notSupported = New List(Of Integer)

            For Each x As Integer In requiredVersions
                If Not dataIn.ServerSupportedFamilyIds.Contains(x) Then
                    notSupported.Add(x)
                End If
            Next

            If notSupported.Count = 0 Then
                Dim dataOut As Snac0117 = New Snac0117

                dataOut.FamilyNameVersionPairs.Add(New FamilyVersionPair(&H1, &H4))
                dataOut.FamilyNameVersionPairs.Add(New FamilyVersionPair(&H13, &H4))
                dataOut.FamilyNameVersionPairs.Add(New FamilyVersionPair(&H2, &H1))
                dataOut.FamilyNameVersionPairs.Add(New FamilyVersionPair(&H3, &H1))
                dataOut.FamilyNameVersionPairs.Add(New FamilyVersionPair(&H15, &H1))
                dataOut.FamilyNameVersionPairs.Add(New FamilyVersionPair(&H4, &H1))
                dataOut.FamilyNameVersionPairs.Add(New FamilyVersionPair(&H6, &H1))
                dataOut.FamilyNameVersionPairs.Add(New FamilyVersionPair(&H9, &H1))
                dataOut.FamilyNameVersionPairs.Add(New FamilyVersionPair(&HA, &H1))
                dataOut.FamilyNameVersionPairs.Add(New FamilyVersionPair(&HB, &H1))

                Send(dataOut)
            Else
                Throw New NotSupportedException("This server does not support all required features!")
            End If
        Catch ex As Exception
            Core.Kernel.Exceptions.PublishException(ex)
        End Try
    End Sub

    Private Sub AnalyseSnac0118(ByVal dataIn As Snac0118)
        Try
            Dim dataOut As Snac0106 = New Snac0106

            Send(dataOut)
        Catch ex As Exception
            Core.Kernel.Exceptions.PublishException(ex)
        End Try
    End Sub

    Private Sub AnalyseSnac0107(ByVal dataIn As Snac0107)
        Dim serverRateGroupIds As List(Of Integer)
        Dim dataOut As Snac0108
        Dim dataContactListCheckout As Snac

        Try
            serverRateGroupIds = New List(Of Integer)

            For Each x As RateGroup In dataIn.RateGroups
                serverRateGroupIds.Add(x.GroupId)
            Next

            dataOut = New Snac0108

            dataOut.RateGroupIds.AddRange(serverRateGroupIds)

            Dim svStorage = Context.GetService(Of Interfaces.IStorageService)()

            If svStorage.Info IsNot Nothing Then
                Core.Kernel.Logger.Log("IcqInterface.IcqConnector", TraceEventType.Information, "Requesting Contact List Delta, Items: {0}, Changed: {1}", svStorage.Info.ItemCount, svStorage.Info.DateChanged)
                dataContactListCheckout = New Snac1305 With {.ModificationDate = svStorage.Info.DateChanged, .NumberOfItems = svStorage.Info.ItemCount}
            Else
                Core.Kernel.Logger.Log("IcqInterface.IcqConnector", TraceEventType.Information, "Requesting Complete Contact List")
                dataContactListCheckout = New Snac1304
            End If

            Send(dataOut, New Snac010E, New Snac0202, New Snac0302, New Snac0404, New Snac0902, New Snac1302, dataContactListCheckout)
        Catch ex As Exception
            Core.Kernel.Exceptions.PublishException(ex)
        End Try
    End Sub

    Private Sub AnalyseSnac1306(ByVal dataIn As Snac1306)
        Dim dataOut As Snac1307

        Try
            dataOut = New Snac1307

            Send(dataOut)

            CompleteInitialization()
        Catch ex As Exception
            Core.Kernel.Exceptions.PublishException(ex)
        End Try
    End Sub

    Private Sub CompleteInitialization()
        Dim snacUserInfo As Snac0204

        ' In the following the client capabilities are propageted to the server. This is
        ' used to show other clients which features of the Icq/Aim network this client
        ' supports.
        ' At the moment only the icq flag to show that this client is an icq client.
        ' When more features are made available more flags have to be set.
        snacUserInfo = New Snac0204
        snacUserInfo.Capabilities.Capabilites.Add(DataTypes.IcqClientCapabilities.IcqFlag)

        ' The following sets up the message channels the client understands.
        ' Channel 1: Plain text messages
        ' Channel 2: Rich text messages and other communications
        ' Channel 4: obsolete

        Dim confChannel01 As Snac0402

        confChannel01 = New Snac0402
        confChannel01.Channel = 1
        confChannel01.MessageFlags = &HB
        confChannel01.MaxMessageSnacSize = &H1F40
        confChannel01.MaxSenderWarningLevel = &H3E7
        confChannel01.MaxReceiverWarningLevel = &H3E7
        confChannel01.MinimumMessageInterval = 0

        Dim confChannel02 As Snac0402

        confChannel02 = New Snac0402
        confChannel02.Channel = 2
        confChannel02.MessageFlags = &H3
        confChannel02.MaxMessageSnacSize = &H1F40
        confChannel02.MaxSenderWarningLevel = &H3E7
        confChannel02.MaxReceiverWarningLevel = &H3E7
        confChannel02.MinimumMessageInterval = 0

        Dim confChannel04 As Snac0402

        confChannel04 = New Snac0402
        confChannel04.Channel = 4
        confChannel04.MessageFlags = &H3
        confChannel04.MaxMessageSnacSize = &H1F40
        confChannel04.MaxSenderWarningLevel = &H3E7
        confChannel04.MaxReceiverWarningLevel = &H3E7
        confChannel04.MinimumMessageInterval = 0

        ' Set up "DirectConnection" configuration of the client. This is a
        ' peer to peer communication to allow file transfers etc.
        ' At the moment it is set to not supported.

        Dim extendedStatusRequest As Snac011e

        extendedStatusRequest = New Snac011e
        extendedStatusRequest.DCInfo.DcProtocolVersion = 8
        extendedStatusRequest.DCInfo.DcByte = DcType.DirectConnectionDisabledAuthRequired

        ' The user is not idle so set the idle time to zero.

        Dim setIdleTime As Snac0111

        setIdleTime = New Snac0111
        setIdleTime.IdleTime = TimeSpan.FromSeconds(0)

        ' This is used to tell the server the understood services.
        ' Since this is an Icq Client only icq services are listed.

        Dim supportedServices As Snac0102
        supportedServices = New Snac0102
        supportedServices.Families.Add(New FamilyIdToolPair(&H1, &H4, &H110, &H8E4))
        supportedServices.Families.Add(New FamilyIdToolPair(&H13, &H4, &H110, &H8E4))
        supportedServices.Families.Add(New FamilyIdToolPair(&H2, &H1, &H110, &H8E4))
        supportedServices.Families.Add(New FamilyIdToolPair(&H3, &H1, &H110, &H8E4))
        supportedServices.Families.Add(New FamilyIdToolPair(&H15, &H1, &H110, &H8E4))
        supportedServices.Families.Add(New FamilyIdToolPair(&H4, &H1, &H110, &H8E4))
        supportedServices.Families.Add(New FamilyIdToolPair(&H6, &H1, &H110, &H8E4))
        supportedServices.Families.Add(New FamilyIdToolPair(&H9, &H1, &H110, &H8E4))
        supportedServices.Families.Add(New FamilyIdToolPair(&HA, &H1, &H110, &H8E4))
        supportedServices.Families.Add(New FamilyIdToolPair(&HB, &H1, &H110, &H8E4))

        Send(snacUserInfo, confChannel01, confChannel02, confChannel04, extendedStatusRequest, setIdleTime, supportedServices)

        ' It is required to run the completion asynchronous. otherwise this call would block the analyzation of
        ' data in the analyzation pipe.
        Threading.ThreadPool.QueueUserWorkItem(New Threading.WaitCallback(AddressOf AsyncCompleteSignIn))
    End Sub

    Private Sub AsyncCompleteSignIn(ByVal state As Object)
        Dim svStorage As Interfaces.IStorageService
        Dim waitForContactList As Threading.EventWaitHandle

        ' now we have to wait for the IcqStorage service to finish contact list analyzation
        ' the easiest way to do so is blocking the current thread until the ContactListActivated
        ' event is fired.

        svStorage = Context.GetService(Of Interfaces.IStorageService)()
        waitForContactList = New Threading.ManualResetEvent(False)

        ' That's why I love linq :)
        AddHandler svStorage.ContactListActivated, Function(sender As Object, e As EventArgs) _
                                                       waitForContactList.Set

        If Not waitForContactList.WaitOne(TimeSpan.FromSeconds(10), True) Then
            OnSignInFailed("Timeout while waiting for server response.")
        Else
            Context.GetService(Of Interfaces.IUserInformationService).RequestShortUserInfo(Context.Identity)

            OnSignInCompleted()
        End If
    End Sub

End Class

Public Class PasswordCredential
    Implements Interfaces.IPasswordCredential

    Private _Password As String

    Public Sub New(ByVal pwd As String)
        _Password = pwd
    End Sub

    Public ReadOnly Property Password() As String Implements Interfaces.IPasswordCredential.Password
        Get
            Return _Password
        End Get
    End Property
End Class

Public Class IcqException
    Inherits ApplicationException

    Public Sub New(ByVal serviceId As Integer, ByVal code As IcqInterface.DataTypes.ErrorCode, ByVal subcode As Integer)
        MyBase.New(String.Format("Service: {0} ErrorCode: {1} ErrorSubCode: {2}", serviceId, code, subcode))
        _ServiceId = serviceId
        _ErrorCode = code
        _ErrorSubCode = subcode
    End Sub

    Private _ErrorCode As IcqInterface.DataTypes.ErrorCode
    Public ReadOnly Property ErrorCode() As IcqInterface.DataTypes.ErrorCode
        Get
            Return _ErrorCode
        End Get
    End Property

    Private _ErrorSubCode As Integer
    Public ReadOnly Property ErrorSubCode() As Integer
        Get
            Return _ErrorSubCode
        End Get
    End Property

    Private _ServiceId As Integer
    Public ReadOnly Property ServiceId() As Integer
        Get
            Return _ServiceId
        End Get
    End Property
End Class